import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.UUID;

public class Client
{
    class ClientListener implements Runnable
    {
        Client client;

        public ClientListener(Client client)
        {
            this.client = client;
        }

        public void run()
        {
            UDPWrapper listener = client.listeningSocket;

            while (true)
            {
                byte[] buffer = new byte[bufferSize];
                DatagramPacket rawPacket = new DatagramPacket(buffer, bufferSize);
                listener.receive(rawPacket);

                Packet packet = (Packet) Utility.deserializeObj(rawPacket.getData());

                switch (packet.type)
                {
                case ClientTable:
                {
                    updateTable((HashMap<String, ClientStatus>) packet.data);
                    break;
                }
                case ChatMsg:
                {
                    ChatMsg msg = (ChatMsg) packet.data;
                    System.out.println("");
                    System.out.format("%s: <%s>\n%s\n>>>", msg.sender, msg.timestamp.toString(), msg.msg);
                    sendACK(msg.sender, msg.uniqueID);
                    break;
                }
                case ACK:
                {
                    UUID id = (UUID) packet.data;
                    client.removeUnACKed(id);
                    break;
                }
                case OfflineMsg:
                {
                    System.out.println();
                    System.out.println(">>> [You have messages]");
                    ArrayList<ChatMsg> msgs = (ArrayList<ChatMsg>) packet.data;
                    for (ChatMsg msg : msgs)
                    {
                        System.out.format("%s: <%s>\n%s\n>>>", msg.sender, msg.timestamp.toString(), msg.msg);
                    }
                    break;
                }
                default:
                    break;
                }
            }
        }

        synchronized private void updateTable(HashMap<String, ClientStatus> table)
        {
            System.out.println("");
            System.out.println(">>> [Client table updated.]");
            System.out.print(">>>");
            client.table = table;
        }
    }

    class ShutdownHook extends Thread
    {
        Client client;

        public ShutdownHook(Client client)
        {
            this.client = client;
        }

        public void run()
        {
        }
    }

    private String serverIP;
    private int serverPort;
    private int recvPort;
    private String nickname;

    private HashMap<String, ClientStatus> table;

    private HashSet<UUID> unACKed = new HashSet<UUID>();

    private static final int bufferSize = 10000;

    private UDPWrapper listeningSocket;

    private Thread listenThread;

    private boolean isOnline = false;

    public Client(String nickname, String serverIP, int serverPort, int recvPort)
    {
        this.serverIP = serverIP;
        this.serverPort = serverPort;
        this.recvPort = recvPort;
        this.nickname = nickname;

        this.listenThread = new Thread(new ClientListener(this));
    }

    synchronized private void addUnACKed(UUID id)
    {
        unACKed.add(id);
    }

    private void logoff()
    {
        LogoffRequest request;
        int tryCount = 0;
        do
        {
            request = new LogoffRequest(nickname);
            Packet packet = new Packet(Packet.PacketType.LogoffRequest, request);
            UDPWrapper sender = new UDPWrapper();
            sender.send(packet, serverIP, serverPort);
            sender.close();
        } while (!waitForACK(request.id) && ++tryCount < 5);

        if (tryCount == 5)
        {
            System.out.println(">>> [Server not responding]");
            System.out.println(">>> [Exiting]");
            System.exit(1);
        }

        isOnline = false;
        listenThread.stop();
        listeningSocket.close();
        System.out.println(">>> [You are Offline. Bye.]");
    }

    /**
     * @param commandline
     * @return null if command is invalid, an array of delimited parameters
     *         otherwise.
     */
    private String[] parseCommand(String commandline)
    {
        int firstSpace = commandline.indexOf(' ');
        String command = (firstSpace == -1) ? commandline : commandline.substring(0, firstSpace);

        if (command.equals("send"))
        {
            int secondSpace = commandline.indexOf(' ', firstSpace + 1);
            if (secondSpace == -1)
                return null;
            else
            {
                String recipient = commandline.substring(firstSpace + 1, secondSpace);
                String msg = commandline.substring(secondSpace + 1, commandline.length());
                return new String[] { command, recipient, msg };
            }
        }
        else if (command.equals("reg") || command.equals("dereg"))
        {
            int secondSpace = commandline.indexOf(' ', firstSpace + 1);
            if (secondSpace != -1)
                return null;
            else
            {
                String name = commandline.substring(firstSpace + 1, commandline.length());
                return new String[] { command, name };
            }
        }
        else
            return null;
    }

    synchronized private ClientStatus readTable(String nickname)
    {
        return table.get(nickname);
    }

    private void register()
    {
        try
        {
            listeningSocket = new UDPWrapper(recvPort);
        }
        catch (SocketException e)
        {
            System.out.println(">>> [Receive port in use. Exiting...]");
            System.exit(1);
        }
        listenThread = new Thread(new ClientListener(this));
        listenThread.start();

        RegRequest request = new RegRequest(nickname, recvPort);
        Packet packet = new Packet(Packet.PacketType.RegRequest, request);

        UDPWrapper sender = new UDPWrapper();
        sender.send(packet, serverIP, serverPort);
        sender.close();

        if (waitForACK(request.uniqueID))
        {
            System.out.println(" [Welcome, You are registered.]");
            isOnline = true;
        }
        else
        {
            System.out.println(" [Registration failed!]");
            isOnline = false;
            listeningSocket.close();
            listenThread.stop();
        }
    }

    synchronized private boolean removeUnACKed(UUID id)
    {
        return unACKed.remove(id);
    }

    public void run()
    {
        Runtime.getRuntime().addShutdownHook(new ShutdownHook(this));

        register();

        InputStreamReader converter = new InputStreamReader(System.in);
        BufferedReader in = new BufferedReader(converter);
        while (true)
        {
            System.out.print(">>>");
            try
            {
                String commandline = in.readLine();

                if (commandline == null)
                    break;
                else if (commandline.equals(""))
                    continue;

                String[] command = parseCommand(commandline);

                if (command == null)
                {
                    System.out.println("Incorrect command!");
                }
                else if (command[0].equals("send"))
                {
                    if (!isOnline)
                    {
                        System.out.println(">>> [Not online.]");
                    }
                    else
                        sendMsg(command[1], command[2]);
                }
                else if (command[0].equals("dereg"))
                {
                    if (!isOnline)
                    {
                        System.out.println(">>> [Already offline.]");
                        continue;
                    }
                    if (command[1].equals(nickname))
                        logoff();
                    else
                        System.out.println(">>> [Cannot dereg others.]");
                }
                else if (command[0].equals("reg"))
                {
                    if (isOnline)
                    {
                        System.out.println(">>> [Already online.]");
                        continue;
                    }

                    this.nickname = command[1];

                    register();
                }
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

    private void sendACK(String recipient, UUID id)
    {
        UDPWrapper sender = new UDPWrapper();
        Packet packet = new Packet(Packet.PacketType.ACK, id);

        ClientStatus status = readTable(recipient);
        sender.send(packet, status.ipAddress, status.recvPort);
        sender.close();
    }

    private void sendOfflineMsg(ChatMsg msg)
    {
        UDPWrapper sender = new UDPWrapper();
        Packet packet = new Packet(Packet.PacketType.ChatMsg, msg);

        int tryCount = 0;
        do
        {
            tryCount++;
            sender.send(packet, serverIP, serverPort);
        } while (!waitForACK(msg.uniqueID) && ++tryCount < 5);

        sender.close();
        
        if (tryCount < 5)
            System.out.println(">>> [Messages received by the server and saved]");
        else
        {
            System.out.println(">>> [Unable to store message in server, exiting...]");
            System.exit(1);
        }
    }

    private void sendMsg(String recipient, String msg)
    {
        ClientStatus status = readTable(recipient);

        if (status != null)
        {
            UDPWrapper sender = new UDPWrapper();
            ChatMsg chatMsg = new ChatMsg(nickname, recipient, msg);
            Packet packet = new Packet(Packet.PacketType.ChatMsg, chatMsg);

            if (!status.isOnline)
            {
                System.out.println(">>> [Recipient offline. Sending to server.]");
                sendOfflineMsg(chatMsg);
            }
            else
            {
                sender.send(packet, status.ipAddress, status.recvPort);
                sender.close();
                if (!waitForACK(chatMsg.uniqueID))
                {
                    System.out.println(">>> [No ACK from " + recipient + ", message sent to server.]");
                    sendOfflineMsg(chatMsg);
                }
                else
                    System.out.println(">>> [Messages received by " + recipient + "]");
            }
        }
        else
        {
            System.out.println(">>> [Recipient doesn't exist.]");
        }
    }

    private boolean waitForACK(UUID id)
    {
        addUnACKed(id);
        try
        {
            Thread.sleep(500);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        return !removeUnACKed(id);
    }
}
